package ChatSystem;

import java.io.File;
import java.io.IOException;
import java.net.InetAddress;
import java.util.*;


/**
 *  The ChatSystem controller, which manages the connected users and allows communication between the GUI and the Network Interface
 */
public class ChatController {
	private Map<String, User> _users;
	private ArrayList<String> _userNames;
	private ChatNI _ni;
	private ChatGUI _gui;
	private User _self;
	private static ChatController _instance;
	
	/**
	 * Accesses the singleton instance.
	 * @return The ChatController instance of the local ChatSystem
	 */
	public static ChatController getInstance() {
		if(_instance == null) {
			_instance = new ChatController();
		}
		
		return _instance;
	}

	/**
	 * Initializes the ChatController by creating the GUI and network interface
	 */
	private ChatController() {
		_users = new HashMap<String, User>();
		_userNames = new ArrayList<String>();
		_ni = new ChatNI(this);
		_gui = new ChatGUI(this);
	}

	/**
	 * Returns the local user, i.e. the one which is logged in on the machine. The local user is null until login.
	 * @return The local user
	 */
	public User getLocalUser() {
		return _self;
	}

	/**
	 * Gets the user at the given index (the order is the same as presented in the GUI)
	 * @param index The index in the users table
	 * @return The requested user
	 */
	public User getUser(int index) {
		synchronized (_users) {
			return _users.get(_userNames.get(index));
		}
	}

	/**
	 * Gets the user who matches the provided nickname
	 * @param nickname The nickname of the user
	 * @return The requested user
	 */
	public User getUser(String nickname) {
		synchronized (_users) {
			return _users.get(nickname);
		}
	}
	
	/**
	 * Gets the user connected on the machine with the provided Internet address.
	 * @param a	The Internet address of the user
	 * @return The requested user
	 */
	public User getUser(InetAddress a) {
		synchronized(_users) {
			for(Map.Entry<String, User> e : getUsersMap().entrySet()) {
				if(e.getValue().getAddress().equals(a)) {
					return e.getValue();
				}
			}
		}
		
		return null;
	}

	/**
	 * Registers a new user in the ChatSystem
	 * @param nickname The new user's nickname
	 * @param a The new user's Internet address
	 * @return The newly created user
	 */
	public User addUser(String nickname, InetAddress a) {
		synchronized (_users) {
			User user = new User(nickname, a, _ni.getPort());
			
			_userNames.add(nickname);
			_users.put(nickname, user);
			_gui.updateUserList();
			
			return user;
		}
	}

	/**
	 * Removes an user from the ChatSystem
	 * @param nickname The user to remove
	 */
	public void removeUser(String nickname) {
		synchronized (_users) {
			_userNames.remove(nickname);
			_users.remove(nickname);
			_gui.updateUserList();
		}
	}

	/**
	 * Gets the list of the know users' nicknames. 
	 * @return The known users' nicknames
	 */
	public ArrayList<String> getUsers() {
		return _userNames;
	}

	/**
	 * Gets the known users mapped by their nickname.
	 * @return The known users
	 */
	public Map<String, User> getUsersMap() {
		return _users;
	}

	/**
	 * Performs the login operation, sending Hello message and enabling network messages management.
	 * @param nickname The local user's nickname
	 * @throws IOException When an the network interface could not establish a connection, typically when an instance of the ChatSystem is already running.
	 */
	public void connect(String nickname) throws IOException {
		try {
			_self = new User(nickname, null, _ni.getPort());
			
			// Uncomment the following block to automatically add the local user to the available recipients (useful for testing purposes)
			/*{
				// No need for synchronization here as the user discovery is not yet enabled, so no new user are going to pop magically from nowhere.
				_userNames.add(nickname);
				_users.put(nickname, _self);
			}*/
			
			_ni.sendHello();
		}
		catch (IOException e) {
			_gui.presentError("Could not establish a connection, sorry!");
			throw e;
		}
	}

	/**
	 * Logs out the local user from the ChatSystem. No more network messages are handled after that.
	 */
	public void disconnect() {
		if(_ni.isConnected()) {
			_ni.sendGoodbye();
		}
		
		synchronized (_users) {
			_userNames.clear();
			_users.clear();
		}
	}

	/**
	 * Perform a logout operation and cleans up some network resource. This should be the last method call to the ChatController object.
	 */
	public void exit() {
		if(_ni.isConnected()) {
			this.disconnect();
		}

		_ni.stop();
	}

	/**
	 * Updates the user's conversation upon reception of a new message.
	 * @param sender The sender of the message and user whose conversation will be updated
	 * @param message The body of the message to add to the user's conversation
	 */
	public void onMessageReceived(User sender, String message) {
		sender.addUnreadMessage(new ChatMessage.ChatTextMessage(message,
				new Date(), sender, this.getLocalUser()));
		_gui.updateMessageList();
	}

	/**
	 * Sends the message to the provided recipient and updates the related conversation.
	 * @param recipient The recipient of the message
	 * @param message The message's body
	 */
	public void sendMessage(User recipient, String message) {
		ChatMessage.ChatTextMessage txtMsg = new ChatMessage.ChatTextMessage(message,
				new Date(), this.getLocalUser(), recipient);
		recipient.addMessage(txtMsg);
		_ni.sendMessage(txtMsg);
	}
	
	/**
	 * Presents the file request to the local user.
	 * @param user The sender of the file
	 * @param filename The name of the file to be reviewed
	 */
	public void onFileRequestReceived(User user, String filename) {
		_gui.presentFileRequest(user, filename);
	}
	
	/**
	 * Accepts a file request previously presented to the user
	 * @param user The file sender
	 * @param filename The name of the file to be sent
	 * @param saveFile The location where the file will be saved
	 */
	public void acceptFileRequest(User user, String filename, File saveFile) {
		ChatMessage.ChatFileMessage file = new ChatMessage.ChatFileMessage(filename, saveFile,
				new Date(), user, this.getLocalUser());
		user.getMessages().add(file);
		_ni.acceptFileRequest(file);
	}
	
	/**
	 * Declines a file request previously presented to the user
	 * @param user The file sender
	 * @param filename The name of the declined file
	 */
	public void declineFileRequest(User user, String filename) {
		ChatMessage.ChatFileMessage file = new ChatMessage.ChatFileMessage(filename, new File(""),
				new Date(), user, this.getLocalUser());
		
		user.getMessages().add(file);

		file.setState(ChatMessage.ChatFileMessage.State.Declined);
		_ni.declineFileRequest(file);
	}
	
	/**
	 * Notifies the GUI when a file message's status has been changed.
	 * @param message The file message with updated state
	 */
	public void notifyFileStatusChange(ChatMessage.ChatFileMessage message) {
		_gui.updateMessageList();
	}
	
	/**
	 * Forwards the GUI's file request to the NI and updates the user's conversation
	 * @param recipient The recipient of the file request
	 * @param file The file name of the file request
	 */
	public void sendFile(User recipient, File file) {
		ChatMessage.ChatFileMessage fileMessage = new ChatMessage.ChatFileMessage(file.getName(), file,
				new Date(), this.getLocalUser(), recipient);
		
		recipient.addMessage(fileMessage);
		
		_ni.sendFileRequest(fileMessage);
	}
}
